package gameobject.construct;

import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import gameobject.item.Item;
import gameobject.item.RequiredItemHolder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import mygame.DataStore;
import mygame.Inventory;
import mygame.Vector3i;
import mygame.Voxel;
import mygame.VoxelGrid;
import mygame.WorldManager;

public abstract class Construct extends Node {
    public static final Vector3i DEFAULT_BASE_VOXEL_LOCATION = Vector3i.ZERO;
    public static final int DEFAULT_NUM_OCCUPIED_VOXELS = 1;
    protected Vector3i baseVoxelGridLocation;
    protected List<Voxel> occupiedVoxels = new ArrayList<Voxel>();
    protected Node savedParent;

    protected int maxHitPoints = 100;
    protected int hitPoints = 1;
    protected boolean built = false;
    protected Inventory inventory = new Inventory();
    protected EnumMap<RequiredItemHolder, List<Item>> requiredItemsToBuild = new EnumMap<RequiredItemHolder, List<Item>>(RequiredItemHolder.class);
    protected EnumMap<RequiredItemHolder, List<Item>> requiredItemsToHarvest = new EnumMap<RequiredItemHolder, List<Item>>(RequiredItemHolder.class);

    protected void init(Vector3i baseVoxelGridLocation) {
        addModel();
	initOccupiedVoxels();
	setBaseVoxelGridLocation(baseVoxelGridLocation.clone());
	if(this instanceof NaturalConstruct) {
	    setHitPoints(getMaxHitPoints());
	    setBuilt(true);
	} else if(this instanceof BuiltConstruct) {
	    setHitPoints(1);
	    setBuilt(false);
	}
	addStartingInventory();
	initRequiredItems();
    }

    /** Automatically called during creation, this method should attach a model as the first child of the Construct. */
    protected abstract void addModel();

    protected void addModelDefault(Mesh mesh) {
	Geometry fakeModel = new Geometry();
	Material mat = new Material(DataStore.assetManager, "Common/MatDefs/Light/Lighting.j3md");
        fakeModel.setMaterial(mat);
	fakeModel.setMesh(mesh);
	attachChild(fakeModel);
    }

    protected void initOccupiedVoxels() {
	for(int i=0; i<getNumOccupiedVoxels(); i++) {
	    occupiedVoxels.add(new Voxel(this));
	}
    }

    protected abstract int getNumOccupiedVoxels();

    public Vector3i getBaseVoxelGridLocation() {
	return baseVoxelGridLocation;
    }

    public void setBaseVoxelGridLocation(Vector3i baseVoxelGridLocation) {
        if(this.baseVoxelGridLocation == null || !this.baseVoxelGridLocation.equals(baseVoxelGridLocation)) {
            this.baseVoxelGridLocation = baseVoxelGridLocation;
            setLocalTranslationWithRespectToBase();
            recalculateOccupiedVoxels();
        }
    }
    
    /**
     * Sets the local translation of the Construct based on the current baseVoxelGridLocation.
     * This is so the Construct sits in the correct place relative to its base voxel.
     */
    protected abstract void setLocalTranslationWithRespectToBase();
    
    /**
     * A default implementation for setLocalTranslationWithRespectToBase.
     * Convenience method for Constructs which occupy only one voxel.
     */
    protected void setLocalTranslationWithRespectToBaseDefault() {
        setLocalTranslation(VoxelGrid.getWorldLocation(baseVoxelGridLocation));
    }

    /**
     * Recalculates occupiedVoxels. occupiedVoxels is assumed to be filled with the number of voxels returned by
     * getNumOccupiedVoxels(), but with incorrect voxelGridLocations. baseVoxelGridLocation is assumed to be set
     * correctly, and should be used as the base initial value from which the occupiedVoxels array is calculated.
     * The voxels in occupiedVoxels should have correct voxelGridLocations when this method finishes.
     */
    protected abstract void recalculateOccupiedVoxels();

    protected abstract void addStartingInventory();

    protected void initRequiredItems() {
	initRequiredItemMaps();
        addRequiredToBuild();
        addRequiredToHarvest();
    }
    
    protected void initRequiredItemMaps() {
	for(RequiredItemHolder holder : RequiredItemHolder.values()) {
	    requiredItemsToBuild.put(holder, new ArrayList<Item>());
	    requiredItemsToHarvest.put(holder, new ArrayList<Item>());
	}
    }
    
    protected abstract void addRequiredToBuild();
    
    protected abstract void addRequiredToHarvest();

    /** Returns a list of the voxels which are occupied by this Construct. This list should not be modified. */
    public List<Voxel> getOccupiedVoxels() {
	return occupiedVoxels;
    }

    public void instantiate() {
	addPhysics();
    }

    protected void addPhysics() {
        addControl(new RigidBodyControl(0f));
    }
    
    @Override
    public void write(JmeExporter ex) throws IOException {
        super.write(ex);
	OutputCapsule capsule = ex.getCapsule(this);
        capsule.write(baseVoxelGridLocation, "baseVoxelGridLocation", null);
        capsule.writeSavableArrayList(new ArrayList<Voxel>(occupiedVoxels), "occupiedVoxels", null);
	capsule.write(savedParent, "savedParent", null);
	//TODO add hit points / move hit points & built into a status object or something
	capsule.write(inventory, "inventory", null);
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public void read(JmeImporter im) throws IOException {
        super.read(im);
	InputCapsule capsule = im.getCapsule(this);
        baseVoxelGridLocation = (Vector3i)capsule.readSavable("baseVoxelGridLocation", null);
        occupiedVoxels = capsule.readSavableArrayList("occupiedVoxels", null);
	savedParent = (Node)capsule.readSavable("savedParent", null);
	inventory = (Inventory)capsule.readSavable("inventory", null);
	initRequiredItems();
    }

    @Override
    public String toString() {
	return getClass().getSimpleName();
    }

    public Node getSavedParent() {
	return savedParent;
    }

    public void setSavedParent(Node savedParent) {
	this.savedParent = savedParent;
    }

    public void reattachToSavedParent() {
	savedParent.attachChild(this);
    }

    public int getHitPoints() {
	return hitPoints;
    }

    public void setHitPoints(int hitPoints) {
	this.hitPoints = hitPoints;
    }

    public void modifyHitPoints(int delta) {
	hitPoints += delta;

	if(hitPoints < 1) {
	    if(isBuilt()) {
		destroy();
	    } else {
		hitPoints = 1;
	    }
	} else if(hitPoints > maxHitPoints) {
	    hitPoints = maxHitPoints;
	    if(!isBuilt()) {
		built = true;
	    }
	}
    }

    public int getMaxHitPoints() {
	return maxHitPoints;
    }

    public void setMaxHitPoints(int maxHitPoints) {
	this.maxHitPoints = maxHitPoints;
    }

    public boolean isBuilt() {
	return built;
    }

    public void setBuilt(boolean fullyBuilt) {
	this.built = fullyBuilt;
    }

    public Inventory getInventory() {
	return inventory;
    }

    public void setInventory(Inventory inventory) {
	this.inventory = inventory;
    }

    public void destroy() {
        built = false;
	WorldManager.removeConstructFromWorld(this);
    }

    public void rotateLeft() {
	//TODO spin me right round - need to recalculate occvoxes based on current facing direction......
	rotate(new Quaternion().fromAngleAxis(-FastMath.PI/2, new Vector3f(0,1,0)));
	recalculateOccupiedVoxels();
    }

    public void rotateRight() {
	rotate(new Quaternion().fromAngleAxis(FastMath.PI/2, new Vector3f(0,1,0)));
	recalculateOccupiedVoxels();
    }

    public EnumMap<RequiredItemHolder, List<Item>> getRequiredItemsToBuild() {
	return requiredItemsToBuild;
    }

    public EnumMap<RequiredItemHolder, List<Item>> getRequiredItemsToHarvest() {
	return requiredItemsToHarvest;
    }
}
